import React from 'react'
import PropTypes from 'prop-types'
import debounce from 'lodash/debounce'

/* NOTE - RJ - 2021-04-09
 *
 * Keep in mind while reading this code that scollTop and scrollHeight can
 * be decimal numbers (with digits after the comma) involving fractions of
 * pixels

 * When determining whether we are at the bottom, we allow the following amount
 * of pixels as a margin.
 */

const SCROLL_THRESHOLD = 2

/* NOTE - RJ - 2021-04-19
 * While loading, a container can resize a lot of times. Debouncing avoids
 * scrolling to many times, which would feel janky and sometimes lead to the
 * wrong scroll position. It also allows scrolling when the size of the
 * container is likely settled
 */
const SCROLL_DEBOUCE_TIME = 500

/*
 * DOC - SG - 2021-04-08
 * This component keeps the scroll position of its root element at the bottom as long as
 * it hasn't been scrolled manually away from the bottom.
 *
 * It properly handles children changing size (height).
 * You can disable its behavior with shouldScrollToBottom=false.

 * You can use isLastItemAddedFromCurrentToken=true to force a scroll to bottom
 * when an element is added even if the scroll wasn't at the bottom.
 */
export class ScrollToBottomWrapper extends React.Component {
  constructor (props) {
    super(props)

    this.atBottom = true
    this.container = null
    this.containerScrollHeight = 0
    this.resizeObserver = new ResizeObserver(this.handleResizeChildren)
  }

  componentWillUnmount () {
    this.resizeObserver.disconnect()
  }

  handleScroll = () => {
    // NOTE - SG - 2021-04-08 - the second test is used to ignore
    // scrollHeight changes not already handled by handleResizeChildren()
    // as it can be called **after** handleScroll() thanks to asynchronism.
    if (!this.container || this.container.scrollHeight !== this.containerScrollHeight) return

    const clientHeight = this.container.clientHeight
    const currentPosition = this.container.scrollHeight - Math.abs(this.container.scrollTop)

    this.atBottom = Math.abs(currentPosition - clientHeight) <= SCROLL_THRESHOLD
  }

  componentDidUpdate () {
    // NOTE - RJ - 2021-04-09 - sometimes the props are updated twice in a row
    // and require a reevaluation as if the container changed size.
    // Worst case, this call is useless and will do nothing
    this.handleResizeChildren()
  }

  handleResizeChildren = debounce(() => {
    const { props } = this

    if (!this.container) return

    // NOTE - SG - 2021-04-08
    // Keep track of the last known scroll height so that scroll events not generated by
    // this function or the user are ignored.
    this.containerScrollHeight = this.container.scrollHeight

    if (!props.shouldScrollToBottom || !(this.atBottom || props.isLastItemAddedFromCurrentToken)) return

    const behavior = props.isLastItemFromCurrentToken ? 'smooth' : 'instant'

    // INFO - SG - 2021-04-08 - using scrollTo() instead of scrollIntoView()
    // avoids to move the view if the wrapper is in a scroll itself.
    const scrollTop = this.container.scrollHeight - this.container.clientHeight

    this.container.scrollTo({
      // INFO - SG - 2021-04-08 - Leave the x scroll unchanged
      left: this.container.scrollLeft,
      top: scrollTop,
      behavior
    })
  }, SCROLL_DEBOUCE_TIME)

  render () {
    const { props } = this

    return (
      <div
        className={props.customClass}
        onScroll={this.handleScroll}
        ref={el => {
          this.container = el
          if (el) {
            this.containerScrollHeight = el.scrollHeight
            this.resizeObserver.disconnect()
            this.resizeObserver.observe(el.firstChild)
          }
        }}
      >
        <div className='ScrollToBottomContents'>
          {props.children}
        </div>
      </div>
    )
  }
}

export default ScrollToBottomWrapper

ScrollToBottomWrapper.propTypes = {
  customClass: PropTypes.string,
  isLastItemAddedFromCurrentToken: PropTypes.bool,
  shouldScrollToBottom: PropTypes.bool
}

ScrollToBottomWrapper.defaultProps = {
  customClass: '',
  isLastItemAddedFromCurrentToken: false,
  shouldScrollToBottom: false
}
